/*
 * Copyright 2006 Matt Jensen
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package org.spectaql.internal.hibernate;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.EntityMode;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.type.Type;
import org.spectaql.internal.metadata.EntityMetadataBuilder;
import org.spectaql.internal.metadata.PersistenceMetadataBuilder;
import org.spectaql.internal.util.ClassUtils;
import org.spectaql.metadata.EntityMetadata;
import org.spectaql.metadata.PersistenceMetadata;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.Map;

/**
 * <p>
 * <code>HibernateUtils</code> provides utility methods for dealing with common Hibernate persistence tasks.
 * </p>
 */
public class HibernateUtils
{
    private static Log LOG = LogFactory.getLog(HibernateUtils.class);

    /**
     * <p>
     * Construct a {@link HibernateUtils} instance.
     * </p>
     */
    private HibernateUtils()
    {
        super();
    }

    /**
     * <p>
     * Build a {@link PersistenceMetadata} instance using the Hibernate session that is available through a given
     * session locator.
     * </p>
     *
     * @param session the Hibernate session.
     * @return {@link PersistenceMetadata} instance.
     */
    public static PersistenceMetadata createPersistenceMetadata(final Session session)
    {
        return createPersistenceMetadataForSession(session);
    }

    /**
     * <p>
     * Given a Hibernate {@link Session} instance, build a {@link PersistenceMetadata} instance describing its entity
     * mappings and other relevant metadata.
     * </p>
     *
     * @param session the Hibernate session.
     * @return {@link PersistenceMetadata} instance.
     */
    private static PersistenceMetadata createPersistenceMetadataForSession(final Session session)
    {
        // Get the Hibernate metadata and build a PersistenceMetadata instance.
        final PersistenceMetadataBuilder persistenceMetadataBuilder = new PersistenceMetadataBuilder();
        final SessionFactory sessionFactory = session.getSessionFactory();
        @SuppressWarnings("unchecked")
        final Map<String, ClassMetadata> allClassMetadata = sessionFactory.getAllClassMetadata();
        for (final Map.Entry<String, ClassMetadata> entry : allClassMetadata.entrySet())
        {
            // Get metadata for the next entity.
            final String entityName = entry.getKey();
            final ClassMetadata classMetadata = entry.getValue();
            final Class<?> entityClass = classMetadata.getMappedClass(EntityMode.POJO);

            // Create and add the entity metadata.
            final EntityMetadata entityMetadata = createEntityMetadata(persistenceMetadataBuilder, entityClass,
                    entityName, classMetadata);
            persistenceMetadataBuilder.addEntity(entityMetadata);
        }

        // Create and return the metadata object.
        final PersistenceMetadata metadata = persistenceMetadataBuilder.toPersistenceMetadata();
        if (LOG.isDebugEnabled())
        {
            LOG.debug(String.format("Returning metadata %s from session factory %s.", metadata, sessionFactory));
        }
        return metadata;
    }

    /**
     * <p>
     * Create an {@link EntityMetadata} instance describing a given mapped class.
     * </p>
     *
     * @param persistenceMetadataBuilder the persistence metadata builder.
     * @param entityClass                the entity class.
     * @param entityName                 the entity name.
     * @param classMetadata              the Hibernate class metadata.
     * @return {@link EntityMetadata} instance.
     */
    private static EntityMetadata createEntityMetadata(final PersistenceMetadataBuilder persistenceMetadataBuilder,
                                                       final Class<?> entityClass,
                                                       final String entityName,
                                                       final ClassMetadata classMetadata)
    {
        // Build an entity metadata instance.
        final Class<?> identifierClass = classMetadata.getIdentifierType().getReturnedClass();
        final String identifierPropertyName = classMetadata.getIdentifierPropertyName();
        final EntityMetadataBuilder entityMetadataBuilder = persistenceMetadataBuilder.buildEntity(entityName,
                entityClass, identifierClass);
        final String[] propertyNames = classMetadata.getPropertyNames();
        final boolean[] propertyNullabilities = classMetadata.getPropertyNullability();
        final Type[] propertyTypes = classMetadata.getPropertyTypes();
        final Map<String, PropertyDescriptor> propertyDescriptors = ClassUtils.getPropertyDescriptorMap(entityClass);
        for (int i = 0;i < propertyNames.length;++i)
        {
            // Get metadata for the next entity property.
            final String propertyName = propertyNames[i];
            final Class<?> propertyClass = propertyTypes[i].getReturnedClass();
            final boolean propertyNullability = propertyNullabilities[i];

            // Get the property read and write methods.
            final Method propertyReadMethod;
            final Method propertyWriteMethod;
            final PropertyDescriptor propertyDescriptor = propertyDescriptors.get(propertyName);
            if (null != propertyDescriptor)
            {
                propertyReadMethod = propertyDescriptor.getReadMethod();
                propertyWriteMethod = propertyDescriptor.getWriteMethod();
            }
            else
            {
                propertyReadMethod = null;
                propertyWriteMethod = null;
                if (LOG.isWarnEnabled())
                {
                    LOG.warn(String.format(
                            "You will be unable to include property %s of entity class %s in expectation queries because its property descriptor was not found.",
                            propertyName, entityClass));
                }
            }

            // Add the property to the entity metadata builder.
            final boolean isIdentifierProperty = propertyName.equals(identifierPropertyName);
            entityMetadataBuilder.addProperty(propertyName, propertyClass, propertyNullability, propertyReadMethod,
                    propertyWriteMethod, isIdentifierProperty);
        }

        // Create and return the metadata object.
        final EntityMetadata entityMetadata = entityMetadataBuilder.toEntityMetadata();
        if (LOG.isDebugEnabled())
        {
            LOG.debug(String.format("Returning metadata %s for entity class %s.", entityMetadata, entityClass));
        }
        return entityMetadata;
    }
}
